import asyncio
import time


class Coroutines:
    @staticmethod
    def main():
        asyncio.run(Coroutines.first())
        asyncio.run(Coroutines.await_fun())
        asyncio.run(Coroutines.await_task())
        asyncio.run(Coroutines.await_task_group())
        asyncio.run(Coroutines.awaitables())
        asyncio.run(Coroutines.run_tasks())
        asyncio.run(Coroutines.await_timeout())

    @staticmethod
    async def first():
        print('hello')
        await asyncio.sleep(1)
        print('world')

    @staticmethod
    async def say_after(delay, what):
        await asyncio.sleep(delay)
        print(what)

    @staticmethod
    async def await_fun():
        print(f"started at {time.strftime('%X')}")

        await Coroutines.say_after(1, 'hello')
        await Coroutines.say_after(2, 'world')

        print(f"finished at {time.strftime('%X')}")

    @staticmethod
    async def await_task():
        task1 = asyncio.create_task(Coroutines.say_after(1, 'hello'))

        task2 = asyncio.create_task(Coroutines.say_after(2, 'world'))

        print(f"started at {time.strftime('%X')}")

        # Wait until both tasks are completed (should take
        # around 2 seconds.)
        await task1
        await task2

        print(f"finished at {time.strftime('%X')}")

    @staticmethod
    async def await_task_group():
        async with asyncio.TaskGroup() as tg:
            task1 = tg.create_task(Coroutines.say_after(1, 'hello'))
            task2 = tg.create_task(Coroutines.say_after(2, 'world'))

            print(f"started at {time.strftime('%X')}")

        # The await is implicit when the context manager exits.
        print(f"finished at {time.strftime('%X')}")

    @staticmethod
    async def nested():
        return 42

    @staticmethod
    async def awaitables():
        # Nothing happens if we just call "nested()".
        # A coroutine object is created but not awaited,
        # so it *won't run at all*.
        Coroutines.nested()

        # Let's do it differently now and await it:
        print(await Coroutines.nested())  # will print "42".

    @staticmethod
    async def factorial(name, number):
        f = 1
        for i in range(2, number + 1):
            print(f"Task {name}: Compute factorial({number}), currently i={i}...")
            await asyncio.sleep(1)
            f *= i
        print(f"Task {name}: factorial({number}) = {f}")
        return f

    @staticmethod
    async def run_tasks():
        # Schedule three calls *concurrently*:
        L = await asyncio.gather(
            Coroutines.factorial("A", 2),
            Coroutines.factorial("B", 3),
            Coroutines.factorial("C", 4),
        )
        print(L)

    @staticmethod
    async def await_timeout():
        try:
            async with asyncio.timeout(1):
                L = await asyncio.gather(
                    Coroutines.factorial("A", 2),
                    Coroutines.factorial("B", 3),
                    Coroutines.factorial("C", 4),
                )
                print(L)
        except TimeoutError:
            print("The long operation timed out, but we've handled it.")


if __name__ == "__main__":
    Coroutines.main()
